package redis

import (
	"context"
	"fmt"
	"strings"
	"time"

	"gitee.com/zawei/yggdrasil/pkg/lib"
	"gitee.com/zawei/yggdrasil/pkg/log"
	"gitee.com/zawei/yggdrasil/pkg/metrics"
	"github.com/go-redis/redis"
	"github.com/opentracing/opentracing-go"
	"github.com/opentracing/opentracing-go/ext"
)

type Processes func(func(redis.Cmder) error) func(redis.Cmder) error

type PipelineProcesses func(func([]redis.Cmder) error) func([]redis.Cmder) error

func traceProcess(ctx context.Context) Processes {
	return func(next func(redis.Cmder) error) func(redis.Cmder) error {
		return func(cmd redis.Cmder) error {
			span, _ := opentracing.StartSpanFromContext(ctx, strings.ToUpper(cmd.Name()))
			ext.SpanKind.Set(span, "client")
			ext.DBType.Set(span, "redis")
			ext.DBStatement.Set(span, fmt.Sprintf("%v", cmd.Args()))
			defer span.Finish()

			return next(cmd)
		}
	}
}

func tracePipelineProcess(ctx context.Context) PipelineProcesses {
	return func(next func([]redis.Cmder) error) func([]redis.Cmder) error {
		return func(cmds []redis.Cmder) error {
			pipelineSpan, ctx := opentracing.StartSpanFromContext(ctx, "(pipeline)")
			ext.DBType.Set(pipelineSpan, "redis")
			ext.SpanKind.Set(pipelineSpan, "client")

			for i := len(cmds); i > 0; i-- {
				cmdName := strings.ToUpper(cmds[i-1].Name())
				if cmdName == "" {
					cmdName = "(empty command)"
				}
				span, _ := opentracing.StartSpanFromContext(ctx, cmdName)
				ext.DBStatement.Set(span, fmt.Sprintf("%v", cmds[i-1].Args()))
				defer span.Finish()
			}

			defer pipelineSpan.Finish()

			return next(cmds)
		}
	}
}

func slowLoggerProcess(slowThreshold time.Duration) Processes {
	return func(next func(redis.Cmder) error) func(redis.Cmder) error {
		return func(cmd redis.Cmder) error {
			begin := time.Now()
			err := next(cmd)
			cost := time.Since(begin)
			if slowThreshold < cost {
				log.Warnf("redis slow: cost[%s], cmd:[%s]", cost, fmt.Sprintf("%v", cmd.Args()))
			}
			return err
		}
	}
}

func slowLoggerPipelineProcess(slowThreshold time.Duration) PipelineProcesses {
	return func(next func([]redis.Cmder) error) func([]redis.Cmder) error {
		return func(cmds []redis.Cmder) error {
			begin := time.Now()

			if err := next(cmds); err != nil {
				return err
			}

			cost := time.Since(begin)
			if slowThreshold < cost {
				cmdsStr := make([]string, len(cmds))
				for i := len(cmds); i > 0; i-- {
					cmdsStr[i] = fmt.Sprintf("%v", cmds[i].Args())
				}
				log.Warnf("redis slow: cost[%s], cmd:[%s]", cost, fmt.Sprintf("%v", cmdsStr))
			}
			return nil
		}
	}
}

func metricProcess(addr string) Processes {
	return func(next func(redis.Cmder) error) func(redis.Cmder) error {
		return func(cmd redis.Cmder) error {
			begin := time.Now()
			err := next(cmd)
			cost := time.Since(begin)
			cmdsStr := fmt.Sprintf("%v", cmd.Args())
			if err != nil {
				_ = metrics.CounterAdd(lib.MetricsLibHandleTotal, 1, map[string]string{
					"type": "redis", "method": cmdsStr, "address": addr, "code": "ERR"})
			} else {
				_ = metrics.CounterAdd(lib.MetricsLibHandleTotal, 1, map[string]string{
					"type": "redis", "method": cmdsStr, "address": addr, "code": "OK"})
			}
			_ = metrics.HistogramObserve(lib.MetricsLibHandleSec, cost.Seconds(), map[string]string{
				"type": "redis", "method": cmdsStr, "address": addr})
			return err
		}
	}
}

func metricPipelineProcess(addr string) PipelineProcesses {
	return func(next func([]redis.Cmder) error) func([]redis.Cmder) error {
		return func(cmds []redis.Cmder) error {
			beg := time.Now()
			err := next(cmds)
			cost := time.Since(beg)
			for i := 0; i < len(cmds); i++ {
				cmdsStr := fmt.Sprintf("%v", cmds[i].Args())
				if cmds[i].Err() != nil {
					_ = metrics.CounterAdd(lib.MetricsLibHandleTotal, 1, map[string]string{
						"type": "redis", "method": cmdsStr, "address": addr, "code": "ERR"})
				} else {
					_ = metrics.CounterAdd(lib.MetricsLibHandleTotal, 1, map[string]string{
						"type": "redis", "method": cmdsStr, "address": addr, "code": "OK"})
				}
			}
			_ = metrics.HistogramObserve(lib.MetricsLibHandleSec, cost.Seconds(), map[string]string{
				"type": "redis", "method": "pipeline", "address": addr})
			return err
		}
	}
}
